#include "legacy_gps.h"
#include <hardware/gps.h>

#define LOG_TAG "legacy_gps"
#include <utils/Log.h>

#include <string.h>
#include <pthread.h>

static GpsCallbacks sGpsCallbacks;
static AGpsCallbacks sAGpsCallbacks;
static GpsXtraCallbacks sGpsXtraCallbacks;
static const LegacyGpsInterface*  sLegacyGpsInterface = NULL;
static const LegacyAGpsInterface*  sLegacyAGpsInterface = NULL;
static const LegacyGpsXtraInterface*  sLegacyGpsXtraInterface = NULL;

static pthread_mutex_t sEventMutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t sEventCond = PTHREAD_COND_INITIALIZER;
static pthread_mutex_t sAGpsEventMutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t sAGpsEventCond = PTHREAD_COND_INITIALIZER;
static pthread_mutex_t sXtraEventMutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t sXtraEventCond = PTHREAD_COND_INITIALIZER;

// data written to by GPS callbacks
static GpsLocation  sGpsLocation;
static GpsStatus    sGpsStatus;
static GpsSvStatus  sGpsSvStatus;
static AGpsStatus   sAGpsStatus;

// buffer for NMEA data
#define NMEA_SENTENCE_LENGTH    128
#define NMEA_SENTENCE_COUNT     8
struct NmeaSentence {
    GpsUtcTime  timestamp;
    char        nmea[NMEA_SENTENCE_LENGTH];
    int         length;
};
static struct NmeaSentence sNmeaBuffer[NMEA_SENTENCE_COUNT];
static int mNmeaSentenceCount = 0;

static GpsLocation  sGpsLocationCopy;
static GpsStatus    sGpsStatusCopy;
static GpsSvStatus  sGpsSvStatusCopy;
static AGpsStatus   sAGpsStatusCopy;
static struct NmeaSentence sNmeaBufferCopy[NMEA_SENTENCE_COUNT];

enum CallbackType {
    kLocation = 1,
    kStatus = 2,
    kSvStatus = 4,
    kNmeaAvailable = 8,
    kAGpsStatus = 16,
    kXtraDownloadRequest = 32,
}; 
static int sPendingCallbacks = 0;
static int sAGpsPendingCallbacks = 0;
static int sXtraPendingCallbacks = 0;

// agps

static void
legacy_agps_status(LegacyAGpsStatus* legacy_status)
{
    pthread_mutex_lock(&sAGpsEventMutex);

    sAGpsPendingCallbacks |= kAGpsStatus;
    sAGpsStatus.size = sizeof(AGpsStatus);
    sAGpsStatus.type = legacy_status->type;
    sAGpsStatus.status = legacy_status->status;

    pthread_cond_signal(&sAGpsEventCond);
    pthread_mutex_unlock(&sAGpsEventMutex);
}

static void
agps_event_proc(void *arg)
{
    while (1) {
        pthread_mutex_lock(&sAGpsEventMutex);
        while (sAGpsPendingCallbacks == 0) {
            pthread_cond_wait(&sAGpsEventCond, &sAGpsEventMutex);
        }

        // copy and clear the callback flags
        int agpsPendingCallbacks = sAGpsPendingCallbacks;
        sAGpsPendingCallbacks = 0;

        if (agpsPendingCallbacks & kAGpsStatus)
            memcpy(&sAGpsStatusCopy, &sAGpsStatus, sizeof(sAGpsStatusCopy));
        pthread_mutex_unlock(&sAGpsEventMutex);  

        if (agpsPendingCallbacks & kAGpsStatus) {
            LOGE("sAGpsCallbacks.status_cb()");
            sAGpsCallbacks.status_cb(&sAGpsStatusCopy);
        }
    }
}

static LegacyAGpsCallbacks sLegacyAGpsCallbacks =
{
    legacy_agps_status,
};

static void
agps_init(AGpsCallbacks* callbacks)
{
    sAGpsCallbacks.status_cb = callbacks->status_cb;
    sAGpsCallbacks.create_thread_cb = callbacks->create_thread_cb;

    sAGpsCallbacks.create_thread_cb("legacy_agps", agps_event_proc, NULL);

    return sLegacyAGpsInterface->init(&sLegacyAGpsCallbacks);
}

static int
agps_data_conn_open(const char* apn)
{
    return sLegacyAGpsInterface->data_conn_open(apn);
}

static int
agps_data_conn_closed()
{
    return sLegacyAGpsInterface->data_conn_closed();
}

static int
agps_data_conn_failed()
{
    return sLegacyAGpsInterface->data_conn_failed();
}

static int
agps_set_server(AGpsType type, const char* hostname, int port)
{
    return sLegacyAGpsInterface->set_server(type, hostname, port);
}

static const AGpsInterface sAGpsInterface =
{
    sizeof(AGpsInterface),
    agps_init,
    agps_data_conn_open,
    agps_data_conn_closed,
    agps_data_conn_failed,
    agps_set_server,
};

// gps xtra

static void
legacy_xtra_download_request()
{
    pthread_mutex_lock(&sXtraEventMutex);

    sXtraPendingCallbacks |= kXtraDownloadRequest;

    pthread_cond_signal(&sXtraEventCond);
    pthread_mutex_unlock(&sXtraEventMutex);
}

static void
xtra_event_proc(void *arg)
{
    while (1) {
        pthread_mutex_lock(&sXtraEventMutex);
        while (sXtraPendingCallbacks == 0) {
            pthread_cond_wait(&sXtraEventCond, &sXtraEventMutex);
        }

        // copy and clear the callback flags
        int xtraPendingCallbacks = sXtraPendingCallbacks;
        sXtraPendingCallbacks = 0;

        pthread_mutex_unlock(&sXtraEventMutex);  

        if (xtraPendingCallbacks & kXtraDownloadRequest) {
            LOGE("sGpsXtraCallbacks.download_request_cb()");
            sGpsXtraCallbacks.download_request_cb();
        }
    }
}

static LegacyGpsXtraCallbacks sLegacyGpsXtraCallbacks =
{
    legacy_xtra_download_request,
};

static int
xtra_init(GpsXtraCallbacks* callbacks)
{
    sGpsXtraCallbacks.download_request_cb = callbacks->download_request_cb;
    sGpsXtraCallbacks.create_thread_cb = callbacks->create_thread_cb;

    sGpsXtraCallbacks.create_thread_cb("legacy_xtra", xtra_event_proc, NULL);

    return sLegacyGpsXtraInterface->init(&sLegacyGpsXtraCallbacks);
}

static int
xtra_inject_xtra_data(char* data, int length)
{
    return sLegacyGpsXtraInterface->inject_xtra_data(data, length);
}

static const GpsXtraInterface sGpsXtraInterface =
{
    sizeof(GpsXtraInterface),
    xtra_init,
    xtra_inject_xtra_data,
};

// gps

static void
legacy_gps_location(LegacyGpsLocation* legacy_location)
{
    pthread_mutex_lock(&sEventMutex);

    sPendingCallbacks |= kLocation;
    sGpsLocation.size = sizeof(GpsLocation);
    sGpsLocation.flags = legacy_location->flags;
    sGpsLocation.latitude = legacy_location->latitude;
    sGpsLocation.longitude = legacy_location->longitude;
    sGpsLocation.altitude = legacy_location->altitude;
    sGpsLocation.speed = legacy_location->speed;
    sGpsLocation.bearing = legacy_location->bearing;
    sGpsLocation.accuracy = legacy_location->accuracy;
    sGpsLocation.timestamp = legacy_location->timestamp;

    pthread_cond_signal(&sEventCond);
    pthread_mutex_unlock(&sEventMutex);
}

static void
legacy_gps_status(LegacyGpsStatus* legcay_status)
{
    pthread_mutex_lock(&sEventMutex);

    sPendingCallbacks |= kStatus;
    sGpsStatus.size = sizeof(GpsStatus);
    sGpsStatus.status = legcay_status->status;

    pthread_cond_signal(&sEventCond);
    pthread_mutex_unlock(&sEventMutex);
}

static void
legacy_gps_sv_status(LegacyGpsSvStatus* legcay_sv_status)
{
    int i;

    pthread_mutex_lock(&sEventMutex);

    sPendingCallbacks |= kSvStatus;
    sGpsSvStatus.size = sizeof(GpsSvStatus);
    sGpsSvStatus.num_svs = legcay_sv_status->num_svs;
    sGpsSvStatus.ephemeris_mask = legcay_sv_status->ephemeris_mask;
    sGpsSvStatus.almanac_mask = legcay_sv_status->almanac_mask;
    sGpsSvStatus.used_in_fix_mask = legcay_sv_status->used_in_fix_mask;
    for (i = 0; i < sGpsSvStatus.num_svs; i++) {
        sGpsSvStatus.sv_list[i].size = sizeof(GpsSvInfo);
        sGpsSvStatus.sv_list[i].prn = legcay_sv_status->sv_list[i].prn;
        sGpsSvStatus.sv_list[i].snr = legcay_sv_status->sv_list[i].snr;
        sGpsSvStatus.sv_list[i].elevation = legcay_sv_status->sv_list[i].elevation;
        sGpsSvStatus.sv_list[i].azimuth = legcay_sv_status->sv_list[i].azimuth;
    }

    pthread_cond_signal(&sEventCond);
    pthread_mutex_unlock(&sEventMutex);
}

static void
legacy_gps_nmea(LegacyGpsUtcTime legacy_timestamp, const char* nmea, int length)
{
    pthread_mutex_lock(&sEventMutex);

    if (length >= NMEA_SENTENCE_LENGTH) {
        LOGE("NMEA data too long in nmea_callback (length = %d)", length);
        length = NMEA_SENTENCE_LENGTH - 1;
    }
    if (mNmeaSentenceCount >= NMEA_SENTENCE_COUNT) {
        LOGE("NMEA data overflowed buffer (%i >= %i)", mNmeaSentenceCount, NMEA_SENTENCE_COUNT);
        pthread_mutex_unlock(&sEventMutex);
        return;
    }

    sPendingCallbacks |= kNmeaAvailable;
    sNmeaBuffer[mNmeaSentenceCount].timestamp = legacy_timestamp;
    memcpy(sNmeaBuffer[mNmeaSentenceCount].nmea, nmea, length);
    sNmeaBuffer[mNmeaSentenceCount].nmea[length] = 0;
    sNmeaBuffer[mNmeaSentenceCount].length = length;
    mNmeaSentenceCount++;

    pthread_cond_signal(&sEventCond);
    pthread_mutex_unlock(&sEventMutex);
}

static void
gps_event_proc(void *arg)
{
    int i;

    while (1) {
        pthread_mutex_lock(&sEventMutex);
        while (sPendingCallbacks == 0) {
            pthread_cond_wait(&sEventCond, &sEventMutex);
        }

        // copy and clear the callback flags
        int pendingCallbacks = sPendingCallbacks;
        sPendingCallbacks = 0;
        int nmeaSentenceCount = mNmeaSentenceCount;
        mNmeaSentenceCount = 0;

        // copy everything and unlock the mutex before calling into Java code to avoid the possibility
        // of timeouts in the GPS engine.
        if (pendingCallbacks & kLocation)
            memcpy(&sGpsLocationCopy, &sGpsLocation, sizeof(sGpsLocationCopy));
        if (pendingCallbacks & kStatus)
            memcpy(&sGpsStatusCopy, &sGpsStatus, sizeof(sGpsStatusCopy));
        if (pendingCallbacks & kSvStatus)
            memcpy(&sGpsSvStatusCopy, &sGpsSvStatus, sizeof(sGpsSvStatusCopy));
        if (pendingCallbacks & kNmeaAvailable)
            memcpy(&sNmeaBufferCopy, &sNmeaBuffer, nmeaSentenceCount * sizeof(sNmeaBuffer[0]));
        pthread_mutex_unlock(&sEventMutex);  

        if (pendingCallbacks & kLocation) {
            sGpsCallbacks.location_cb(&sGpsLocation);
        }
        if (pendingCallbacks & kStatus) {
            sGpsCallbacks.status_cb(&sGpsStatusCopy);
        }
        if (pendingCallbacks & kSvStatus) {
            sGpsCallbacks.sv_status_cb(&sGpsSvStatus);
        }
        if (pendingCallbacks & kNmeaAvailable) {
            for (i = 0; i < nmeaSentenceCount; i++) {
                sGpsCallbacks.nmea_cb(sNmeaBufferCopy[i].timestamp,
                                      sNmeaBufferCopy[i].nmea,
                                      sNmeaBufferCopy[i].length);
            }
        }
    }
}

static LegacyGpsCallbacks sLegacyGpsCallbacks =
{
    legacy_gps_location,
    legacy_gps_status,
    legacy_gps_sv_status,
    legacy_gps_nmea,
};

static int
gps_init(GpsCallbacks* callbacks)
{
    sGpsCallbacks.size = callbacks->size;
    sGpsCallbacks.location_cb = callbacks->location_cb;
    sGpsCallbacks.status_cb = callbacks->status_cb;
    sGpsCallbacks.sv_status_cb = callbacks->sv_status_cb;
    sGpsCallbacks.nmea_cb = callbacks->nmea_cb;
    sGpsCallbacks.set_capabilities_cb = callbacks->set_capabilities_cb;
    sGpsCallbacks.acquire_wakelock_cb = callbacks->acquire_wakelock_cb;
    sGpsCallbacks.release_wakelock_cb = callbacks->release_wakelock_cb;
    sGpsCallbacks.create_thread_cb = callbacks->create_thread_cb;

    sGpsCallbacks.create_thread_cb("legacy_gps", gps_event_proc, NULL);

    return sLegacyGpsInterface->init(&sLegacyGpsCallbacks);
}

static int
gps_start(void)
{
    return sLegacyGpsInterface->start();
}

static int
gps_stop(void)
{
    return sLegacyGpsInterface->stop();
}

static void
gps_cleanup(void)
{
    sLegacyGpsInterface->cleanup();
}

static int
gps_inject_time(GpsUtcTime time, int64_t timeReference, int uncertainty)
{
    return sLegacyGpsInterface->inject_time(time, timeReference, uncertainty);
}

static int
gps_inject_location(double latitude, double longitude, float accuracy)
{
    return sLegacyGpsInterface->inject_location(latitude, longitude, accuracy);
}

static void
gps_delete_aiding_data(GpsAidingData flags)
{
    sLegacyGpsInterface->delete_aiding_data(flags);
}

static int
gps_set_position_mode(GpsPositionMode mode, GpsPositionRecurrence recurrence,
        uint32_t min_interval, uint32_t preferred_accuracy, uint32_t preferred_time)
{
    return sLegacyGpsInterface->set_position_mode(mode, 0);
}

static const void*
gps_get_extension(const char* name)
{
    if (strcmp(name, GPS_XTRA_INTERFACE) == 0) {
        sLegacyGpsXtraInterface = sLegacyGpsInterface->get_extension(LEGACY_GPS_XTRA_INTERFACE);
        if (sLegacyGpsXtraInterface == NULL)
            return NULL;
        return &sGpsXtraInterface;
    } else if (strcmp(name, AGPS_INTERFACE) == 0) {
        sLegacyAGpsInterface = sLegacyGpsInterface->get_extension(LEGACY_AGPS_INTERFACE);
        if (sLegacyAGpsInterface == NULL)
            return NULL;
        return &sAGpsInterface;
    } else {
        LOGI("don't support: %s", name);
    }
    return NULL;
}

static const GpsInterface sGpsInterface =
{
    sizeof(GpsInterface),
    gps_init,
    gps_start,
    gps_stop,
    gps_cleanup,
    gps_inject_time,
    gps_inject_location,
    gps_delete_aiding_data,
    gps_set_position_mode,
    gps_get_extension,
};

// export

const GpsInterface*
get_gps_interface()
{
    if (sLegacyGpsInterface == NULL)
        sLegacyGpsInterface = gps_get_hardware_interface();
        if (!sLegacyGpsInterface)
            LOGD("no GPS hardware on this device\n");

    return &sGpsInterface;
}

const GpsInterface* gps__get_gps_interface(struct gps_device_t* dev)
{
    return get_gps_interface();
}

static int open_gps(const struct hw_module_t* module, char const* name,
        struct hw_device_t** device)
{
    struct gps_device_t *dev = malloc(sizeof(struct gps_device_t));
    memset(dev, 0, sizeof(*dev));

    dev->common.tag = HARDWARE_DEVICE_TAG;
    dev->common.version = 0;
    dev->common.module = (struct hw_module_t*)module;
    dev->get_gps_interface = gps__get_gps_interface;

    *device = (struct hw_device_t*)dev;
    return 0;
}

static struct hw_module_methods_t gps_module_methods = {
    .open = open_gps
};

const struct hw_module_t HAL_MODULE_INFO_SYM = {
    .tag = HARDWARE_MODULE_TAG,
    .version_major = 1,
    .version_minor = 0,
    .id = GPS_HARDWARE_MODULE_ID,
    .name = "legacy GPS Module",
    .author = "Zhibin Wu <zbwu86@gmail.com>",
    .methods = &gps_module_methods,
};
